Dataanalyse I

Jeppe Fjeldgaard Qvist

2025-10-10

Dagens program

  1. Sprogmodeller (resterende fra lektion 10)
  2. Fælles gennemgang af meningsfulde eksempler på en SDS (tekst)analyse

Sprogmodeller (slides fra L10)

Analyse-eksempel \(1\)

Komparativ (diskurs-/tekst)analyse af (right wing)politiske fora

  • Data: dksamling.dk og trykkefrihed.dk
    • Struktur: tekststørrelse, struktur, kvalitet
      • Hvad er kilderne? (mainstream media vs. alternativt medie/blog)
      • Hvordan er data indsamlet? (web scraping, API, manuel)
      • Hvilke bias kan være indbygget i udvælgelsen?
      • Hvad er unit of analysis? (artikel, afsnit, sætning)
    • (Etiske) overvejelser: Anonymisering? Forskeransvar?
    • Begrænsninger: Bevidst bias; ikke repræsentativt for samfundet. Echo chamber-effekt maksimeret? Bevidst provokerende indhold? Bots?
    • Muligheder: Hvad tænker I?
import pandas as pd
import numpy as np

# HVAD GØR VI HER?
df1 = pd.read_json('/Users/jeppefl/Library/CloudStorage/OneDrive-AalborgUniversitet/01_work/01_undervisning/02_sds1/03_data/right-wing-sites/DKSamling_20230627.json')

df2 = pd.read_json('/Users/jeppefl/Library/CloudStorage/OneDrive-AalborgUniversitet/01_work/01_undervisning/02_sds1/03_data/right-wing-sites/trykkefrihed_20220824.json')

# HVAD GØR VI HER?
df1['source'] = 'DKsamling'
df2['source'] = 'Trykkefrihed'

# HVAD GØR VI HER? OG HVORFOR
df1['text'] = df1['article_content']
df2['text'] = df2['post content']

# HVAD GØR VI HER? OG HVORFOR
df = pd.concat([df1[['text', 'source']], df2[['text', 'source']]])
df = df.reset_index(drop=True)

# Første beskrivelse af data
print(f"Total antal dokumenter: {len(df)}")
print(f"\nDKsamling: {len(df[df['source']=='DKsamling'])}")
print(f"\nTrykkefrihed: {len(df[df['source']=='Trykkefrihed'])}")
print(f"\nManglende værdier:\n{df.isnull().sum()}")
print(f"\nTomme tekster: {(df['text'].str.len() == 0).sum()}")
print("\nEksempel på en tekst:\n")
print(df['text'].iloc[0])
Total antal dokumenter: 660

DKsamling: 366

Trykkefrihed: 294

Manglende værdier:
text      0
source    0
dtype: int64

Tomme tekster: 2

Eksempel på en tekst:

 Efter årtiers fejlslagen politik bygget på drømmesyner om én verden er virkeligheden vendt tilbage med sin forhammer. Jeg har netop læst to politikere og en universitetsansats bekymrede udsagn om det årti, vi står på tærsklen til. Mogens Lykketoft, Lars Løkke Rasmussen og professor Marlene Wind behøver ikke den store introduktion. Socialdemokraten, Venstremanden og den politiserende professor har meget til fælles: De bryder sig ikke om den uafhængige nationalstat. De anskuer EU som afgørende for Europa og ser gerne mere og mere overstatslighed, og følgelig afskyr de brexit og i øvrigt også Donald Trump. Nationalstater, hvor flertallet ikke ønsker at nedlægge sig selv, søles til som stort set antidemokratiske og som noget, katten har slæbt ind. Mogens Lykketoft: ”I Europa er det meget tydeligt, at genforeningen med udvidelsen af EU har givet flere bølgeskvulp, end man havde forudset. EU’s manglende evne til at håndtere beskyttelsen af de ydre grænser samt hele diskussionen om flygtningestrømmene har fyldt. Det har ført til nye modsætninger og nye populistiske bevægelser i Europa, og man har blandt andet set halvautoritære regimer opstå i Polen og Ungarn. Generelt i Europa har man skærpet beskyttelsen af de ydre grænser og strammet udlændingepolitikken, men uden at kunne levere et fælles svar, som kan hjælpe de steder, hvor problemerne er opstået.” Lars Løkke Rasmussen: ”»Jeg vælger at gå med i Marrakech-aftalen og siger: Vi har millioner af migranter i verden. Nogle er lovlige, andre er illegale. Der er masser af problemer, men vi er nødt til at engagere os i internationale diskussioner, og så kommer hele den yderste højrefløj og siger, at du skal have hugget hænderne af, så du ikke skriver under på aftalen,« siger Lars Løkke og sender samtidig en indirekte budskab til partier som Nye Borgerlige og Dansk Folkeparti ved at stå fast på, at Danmark har brug for internationalisering.” Læs resten på Jyllands-Posten 

Præ-processering af data I

import re

example_text = df['text'].iloc[0]

print(example_text[:300])
print(f"Længde: {len(example_text)} tegn\n")
 Efter årtiers fejlslagen politik bygget på drømmesyner om én verden er virkeligheden vendt tilbage med sin forhammer. Jeg har netop læst to politikere og en universitetsansats bekymrede udsagn om det årti, vi står på tærsklen til. Mogens Lykketoft, Lars Løkke Rasmussen og professor Marlene Wind beh
Længde: 1944 tegn
# HVAD GØR VI HER?
text_1 = re.sub(r'<[^>]+>', '', example_text)

# HVAD GØR VI HER?
text_2 = re.sub(r'http\S+|www.\S+', '', text_1)

# HVAD GØR VI HER?
text_3 = re.sub(r'\S+@\S+', '', text_2)

# HVAD GØR VI HER?
text_4 = re.sub(r'\s+', ' ', text_3).strip()

# HVAD GØR VI HER?
text_clean = text_4.lower()
print("Orginal tekst:")
print(example_text[:300])
print(f"Længde: {len(example_text)} tegn\n")

print("Clean tekst:")
print(text_clean[:300])
print(f"Længde: {len(text_clean)} tegn\n")
Orginal tekst:
 Efter årtiers fejlslagen politik bygget på drømmesyner om én verden er virkeligheden vendt tilbage med sin forhammer. Jeg har netop læst to politikere og en universitetsansats bekymrede udsagn om det årti, vi står på tærsklen til. Mogens Lykketoft, Lars Løkke Rasmussen og professor Marlene Wind beh
Længde: 1944 tegn

Clean tekst:
efter årtiers fejlslagen politik bygget på drømmesyner om én verden er virkeligheden vendt tilbage med sin forhammer. jeg har netop læst to politikere og en universitetsansats bekymrede udsagn om det årti, vi står på tærsklen til. mogens lykketoft, lars løkke rasmussen og professor marlene wind behø
Længde: 1942 tegn

Samme process på en hel DataFrame

# Fjern HTML
df['text'] = df['text'].apply(lambda x: re.sub(r'<[^>]+>', '', x) if pd.notna(x) else '')

# Fjern URLs
df['text'] = df['text'].apply(lambda x: re.sub(r'http\S+|www.\S+', '', x))

# Fjern emails
df['text'] = df['text'].apply(lambda x: re.sub(r'\S+@\S+', '', x))

# Normaliser whitespace
df['text'] = df['text'].apply(lambda x: re.sub(r'\s+', ' ', x).strip())

# Lowercase
df['text_clean'] = df['text'].str.lower()

Præ-processering af data II

import spacy
from tqdm import tqdm

# HVAD GØR VI HER? HVAD SKAL VI HUSKE FOR DET VIRKER? 
nlp = spacy.load("da_core_news_lg")

sample = df['text_clean'].iloc[0]

# HVAD GØR VI HER? 
doc = nlp(sample)

all_tokens = [token.text for token in doc]
print(all_tokens)
print(f"\nTotal: {len(all_tokens)} tokens\n")
['efter', 'årtiers', 'fejlslagen', 'politik', 'bygget', 'på', 'drømmesyner', 'om', 'én', 'verden', 'er', 'virkeligheden', 'vendt', 'tilbage', 'med', 'sin', 'forhammer', '.', 'jeg', 'har', 'netop', 'læst', 'to', 'politikere', 'og', 'en', 'universitetsansats', 'bekymrede', 'udsagn', 'om', 'det', 'årti', ',', 'vi', 'står', 'på', 'tærsklen', 'til', '.', 'mogens', 'lykketoft', ',', 'lars', 'løkke', 'rasmussen', 'og', 'professor', 'marlene', 'wind', 'behøver', 'ikke', 'den', 'store', 'introduktion', '.', 'socialdemokraten', ',', 'venstremanden', 'og', 'den', 'politiserende', 'professor', 'har', 'meget', 'til', 'fælles', ':', 'de', 'bryder', 'sig', 'ikke', 'om', 'den', 'uafhængige', 'nationalstat', '.', 'de', 'anskuer', 'eu', 'som', 'afgørende', 'for', 'europa', 'og', 'ser', 'gerne', 'mere', 'og', 'mere', 'overstatslighed', ',', 'og', 'følgelig', 'afskyr', 'de', 'brexit', 'og', 'i', 'øvrigt', 'også', 'donald', 'trump', '.', 'nationalstater', ',', 'hvor', 'flertallet', 'ikke', 'ønsker', 'at', 'nedlægge', 'sig', 'selv', ',', 'søles', 'til', 'som', 'stort', 'set', 'antidemokratiske', 'og', 'som', 'noget', ',', 'katten', 'har', 'slæbt', 'ind', '.', 'mogens', 'lykketoft', ':', '”', 'i', 'europa', 'er', 'det', 'meget', 'tydeligt', ',', 'at', 'genforeningen', 'med', 'udvidelsen', 'af', 'eu', 'har', 'givet', 'flere', 'bølgeskvulp', ',', 'end', 'man', 'havde', 'forudset', '.', 'eu', '’', 's', 'manglende', 'evne', 'til', 'at', 'håndtere', 'beskyttelsen', 'af', 'de', 'ydre', 'grænser', 'samt', 'hele', 'diskussionen', 'om', 'flygtningestrømmene', 'har', 'fyldt', '.', 'det', 'har', 'ført', 'til', 'nye', 'modsætninger', 'og', 'nye', 'populistiske', 'bevægelser', 'i', 'europa', ',', 'og', 'man', 'har', 'blandt', 'andet', 'set', 'halvautoritære', 'regimer', 'opstå', 'i', 'polen', 'og', 'ungarn', '.', 'generelt', 'i', 'europa', 'har', 'man', 'skærpet', 'beskyttelsen', 'af', 'de', 'ydre', 'grænser', 'og', 'strammet', 'udlændingepolitikken', ',', 'men', 'uden', 'at', 'kunne', 'levere', 'et', 'fælles', 'svar', ',', 'som', 'kan', 'hjælpe', 'de', 'steder', ',', 'hvor', 'problemerne', 'er', 'opstået', '.', '”', 'lars', 'løkke', 'rasmussen', ':', '”', '»', 'jeg', 'vælger', 'at', 'gå', 'med', 'i', 'marrakech-aftalen', 'og', 'siger', ':', 'vi', 'har', 'millioner', 'af', 'migranter', 'i', 'verden', '.', 'nogle', 'er', 'lovlige', ',', 'andre', 'er', 'illegale', '.', 'der', 'er', 'masser', 'af', 'problemer', ',', 'men', 'vi', 'er', 'nødt', 'til', 'at', 'engagere', 'os', 'i', 'internationale', 'diskussioner', ',', 'og', 'så', 'kommer', 'hele', 'den', 'yderste', 'højrefløj', 'og', 'siger', ',', 'at', 'du', 'skal', 'have', 'hugget', 'hænderne', 'af', ',', 'så', 'du', 'ikke', 'skriver', 'under', 'på', 'aftalen', ',', '«', 'siger', 'lars', 'løkke', 'og', 'sender', 'samtidig', 'en', 'indirekte', 'budskab', 'til', 'partier', 'som', 'nye', 'borgerlige', 'og', 'dansk', 'folkeparti', 'ved', 'at', 'stå', 'fast', 'på', ',', 'at', 'danmark', 'har', 'brug', 'for', 'internationalisering', '.', '”', 'læs', 'resten', 'på', 'jyllands-posten']

Total: 352 tokens
# Fjern punktuation
tokens_no_punct = [token.text for token in doc if not token.is_punct]
print(tokens_no_punct)
print(f"\nEfter fjernelse af punktuation: {len(tokens_no_punct)} tokens\n")
['efter', 'årtiers', 'fejlslagen', 'politik', 'bygget', 'på', 'drømmesyner', 'om', 'én', 'verden', 'er', 'virkeligheden', 'vendt', 'tilbage', 'med', 'sin', 'forhammer', 'jeg', 'har', 'netop', 'læst', 'to', 'politikere', 'og', 'en', 'universitetsansats', 'bekymrede', 'udsagn', 'om', 'det', 'årti', 'vi', 'står', 'på', 'tærsklen', 'til', 'mogens', 'lykketoft', 'lars', 'løkke', 'rasmussen', 'og', 'professor', 'marlene', 'wind', 'behøver', 'ikke', 'den', 'store', 'introduktion', 'socialdemokraten', 'venstremanden', 'og', 'den', 'politiserende', 'professor', 'har', 'meget', 'til', 'fælles', 'de', 'bryder', 'sig', 'ikke', 'om', 'den', 'uafhængige', 'nationalstat', 'de', 'anskuer', 'eu', 'som', 'afgørende', 'for', 'europa', 'og', 'ser', 'gerne', 'mere', 'og', 'mere', 'overstatslighed', 'og', 'følgelig', 'afskyr', 'de', 'brexit', 'og', 'i', 'øvrigt', 'også', 'donald', 'trump', 'nationalstater', 'hvor', 'flertallet', 'ikke', 'ønsker', 'at', 'nedlægge', 'sig', 'selv', 'søles', 'til', 'som', 'stort', 'set', 'antidemokratiske', 'og', 'som', 'noget', 'katten', 'har', 'slæbt', 'ind', 'mogens', 'lykketoft', 'i', 'europa', 'er', 'det', 'meget', 'tydeligt', 'at', 'genforeningen', 'med', 'udvidelsen', 'af', 'eu', 'har', 'givet', 'flere', 'bølgeskvulp', 'end', 'man', 'havde', 'forudset', 'eu', 's', 'manglende', 'evne', 'til', 'at', 'håndtere', 'beskyttelsen', 'af', 'de', 'ydre', 'grænser', 'samt', 'hele', 'diskussionen', 'om', 'flygtningestrømmene', 'har', 'fyldt', 'det', 'har', 'ført', 'til', 'nye', 'modsætninger', 'og', 'nye', 'populistiske', 'bevægelser', 'i', 'europa', 'og', 'man', 'har', 'blandt', 'andet', 'set', 'halvautoritære', 'regimer', 'opstå', 'i', 'polen', 'og', 'ungarn', 'generelt', 'i', 'europa', 'har', 'man', 'skærpet', 'beskyttelsen', 'af', 'de', 'ydre', 'grænser', 'og', 'strammet', 'udlændingepolitikken', 'men', 'uden', 'at', 'kunne', 'levere', 'et', 'fælles', 'svar', 'som', 'kan', 'hjælpe', 'de', 'steder', 'hvor', 'problemerne', 'er', 'opstået', 'lars', 'løkke', 'rasmussen', 'jeg', 'vælger', 'at', 'gå', 'med', 'i', 'marrakech-aftalen', 'og', 'siger', 'vi', 'har', 'millioner', 'af', 'migranter', 'i', 'verden', 'nogle', 'er', 'lovlige', 'andre', 'er', 'illegale', 'der', 'er', 'masser', 'af', 'problemer', 'men', 'vi', 'er', 'nødt', 'til', 'at', 'engagere', 'os', 'i', 'internationale', 'diskussioner', 'og', 'så', 'kommer', 'hele', 'den', 'yderste', 'højrefløj', 'og', 'siger', 'at', 'du', 'skal', 'have', 'hugget', 'hænderne', 'af', 'så', 'du', 'ikke', 'skriver', 'under', 'på', 'aftalen', 'siger', 'lars', 'løkke', 'og', 'sender', 'samtidig', 'en', 'indirekte', 'budskab', 'til', 'partier', 'som', 'nye', 'borgerlige', 'og', 'dansk', 'folkeparti', 'ved', 'at', 'stå', 'fast', 'på', 'at', 'danmark', 'har', 'brug', 'for', 'internationalisering', 'læs', 'resten', 'på', 'jyllands-posten']

Efter fjernelse af punktuation: 308 tokens
# Fjern stopord
tokens_no_stop = [token.text for token in doc if not token.is_stop and not token.is_punct]
print(tokens_no_stop)
print(f"\nEfter fjernelse af stopord: {len(tokens_no_stop)} tokens")
['årtiers', 'fejlslagen', 'politik', 'bygget', 'drømmesyner', 'én', 'verden', 'virkeligheden', 'vendt', 'sin', 'forhammer', 'netop', 'læst', 'to', 'politikere', 'universitetsansats', 'bekymrede', 'udsagn', 'årti', 'står', 'tærsklen', 'mogens', 'lykketoft', 'lars', 'løkke', 'rasmussen', 'professor', 'marlene', 'wind', 'behøver', 'store', 'introduktion', 'socialdemokraten', 'venstremanden', 'politiserende', 'professor', 'fælles', 'bryder', 'uafhængige', 'nationalstat', 'anskuer', 'eu', 'afgørende', 'europa', 'ser', 'gerne', 'overstatslighed', 'følgelig', 'afskyr', 'brexit', 'donald', 'trump', 'nationalstater', 'flertallet', 'ønsker', 'nedlægge', 'søles', 'stort', 'set', 'antidemokratiske', 'katten', 'slæbt', 'mogens', 'lykketoft', 'europa', 'tydeligt', 'genforeningen', 'udvidelsen', 'eu', 'givet', 'bølgeskvulp', 'forudset', 'eu', 's', 'manglende', 'evne', 'håndtere', 'beskyttelsen', 'ydre', 'grænser', 'samt', 'hele', 'diskussionen', 'flygtningestrømmene', 'fyldt', 'ført', 'nye', 'modsætninger', 'nye', 'populistiske', 'bevægelser', 'europa', 'set', 'halvautoritære', 'regimer', 'opstå', 'polen', 'ungarn', 'generelt', 'europa', 'skærpet', 'beskyttelsen', 'ydre', 'grænser', 'strammet', 'udlændingepolitikken', 'levere', 'fælles', 'svar', 'hjælpe', 'steder', 'problemerne', 'opstået', 'lars', 'løkke', 'rasmussen', 'vælger', 'gå', 'marrakech-aftalen', 'siger', 'millioner', 'migranter', 'verden', 'lovlige', 'illegale', 'masser', 'problemer', 'nødt', 'engagere', 'internationale', 'diskussioner', 'hele', 'yderste', 'højrefløj', 'siger', 'hugget', 'hænderne', 'skriver', 'aftalen', 'siger', 'lars', 'løkke', 'sender', 'samtidig', 'indirekte', 'budskab', 'partier', 'nye', 'borgerlige', 'dansk', 'folkeparti', 'stå', 'fast', 'danmark', 'brug', 'internationalisering', 'læs', 'resten', 'jyllands-posten']

Efter fjernelse af stopord: 159 tokens
# Lemmatisering
tokens_lemma = [token.lemma_ for token in doc if not token.is_stop and not token.is_punct]
print(tokens_lemma)
print(f"\nEfter lemmatisering: {len(tokens_lemma)} tokens")
['årtier', 'fejlslag', 'politik', 'bygge', 'drømmesyner', 'en', 'verden', 'virkelighed', 'vende', 'sin', 'forhammer', 'netop', 'læse', 'to', 'politiker', 'universitetsansats', 'bekymre', 'udsagn', 'årti', 'stå', 'tærskel', 'mogens', 'lykketoft', 'Lars', 'løkke', 'rasmussen', 'professor', 'marl', 'wind', 'behøve', 'stor', 'introduktion', 'socialdemokrat', 'venstremanden', 'politiserende', 'professor', 'fælles', 'bryde', 'uafhængig', 'nationalstat', 'anskue', 'eu', 'afgørende', 'Europa', 'se', 'gerne', 'overstatslighed', 'følgelig', 'afskyre', 'brexit', 'donald', 'trump', 'nationalstat', 'flertal', 'ønske', 'nedlægge', 'søle', 'stor', 'se', 'antidemokratisk', 'katt', 'slæbe', 'mogens', 'lykketoft', 'Europa', 'tydelig', 'genforening', 'udvidelse', 'eu', 'give', 'bølgeskvulp', 'forudse', 'eu', 's', 'mangle', 'evne', 'håndtere', 'beskyttelse', 'ydre', 'grænse', 'samt', 'hel', 'diskussion', 'flygtningestrømmene', 'fylde', 'føre', 'ny', 'modsætninge', 'ny', 'populistisk', 'bevægelse', 'Europa', 'se', 'halvautoritære', 'regime', 'opstå', 'polen', 'ungarn', 'generelt', 'Europa', 'skærpe', 'beskyttelse', 'ydre', 'grænse', 'stramme', 'udlændingepolitik', 'levere', 'fælles', 'svar', 'hjælpe', 'sted', 'problem', 'opstå', 'Lars', 'løkke', 'rasmussen', 'vælge', 'gå', 'marrakech-aftalen', 'sige', 'million', 'migrant', 'verden', 'lovlig', 'illegal', 'masse', 'problem', 'nødt', 'engagere', 'international', 'diskussion', 'hel', 'yderst', 'højrefløj', 'sige', 'hugge', 'hånd', 'skrive', 'aftale', 'sige', 'Lars', 'løkke', 'sende', 'samtidig', 'indirekte', 'budskab', 'parti', 'ny', 'borgerlig', 'dansk', 'folkeparti', 'stå', 'fast', 'Danmark', 'brug', 'internationalisering', 'læse', 'rest', 'jyllands-posten']

Efter lemmatisering: 159 tokens
tokens_filtered = [token.lemma_ for token in doc 
                    if not token.is_stop 
                    and not token.is_punct 
                    and token.pos_ in ['NOUN', 'ADJ']]
print(tokens_filtered)                  
print(f"\nKun NOUN + ADJ: {len(tokens_filtered)} tokens")
['årtier', 'fejlslag', 'politik', 'drømmesyner', 'verden', 'virkelighed', 'forhammer', 'politiker', 'udsagn', 'årti', 'tærskel', 'professor', 'stor', 'introduktion', 'socialdemokrat', 'venstremanden', 'politiserende', 'professor', 'fælles', 'uafhængig', 'nationalstat', 'afgørende', 'overstatslighed', 'brexit', 'nationalstat', 'flertal', 'søle', 'antidemokratisk', 'katt', 'tydelig', 'genforening', 'udvidelse', 'bølgeskvulp', 'evne', 'beskyttelse', 'ydre', 'grænse', 'hel', 'diskussion', 'flygtningestrømmene', 'ny', 'modsætninge', 'ny', 'populistisk', 'bevægelse', 'halvautoritære', 'regime', 'beskyttelse', 'ydre', 'grænse', 'udlændingepolitik', 'fælles', 'svar', 'sted', 'problem', 'marrakech-aftalen', 'million', 'migrant', 'verden', 'lovlig', 'illegal', 'masse', 'problem', 'nødt', 'international', 'diskussion', 'hel', 'yderst', 'højrefløj', 'hånd', 'aftale', 'indirekte', 'budskab', 'parti', 'ny', 'borgerlig', 'dansk', 'folkeparti', 'brug', 'internationalisering', 'rest']

Kun NOUN + ADJ: 81 tokens

Hvad beslutter vi os for?

Fuld pre-processing m. beslutning

fjern stopord + lemmatisering (tokens_lemma)

# Initialiser tom liste til tokens
all_tokens = []

# Process i batches (for fart ...)
batch_size = 50
texts = df['text_clean'].tolist()

for doc in tqdm(nlp.pipe(texts, batch_size=batch_size), total=len(texts)):

    tokens = [token.lemma_ for token in doc 
              if not token.is_stop 
              and not token.is_punct 
              and not token.is_space]
    all_tokens.append(tokens)

# Tilføj til dataframe
df['tokens'] = all_tokens

# Beregn antal tokens
df['n_tokens'] = df['tokens'].apply(len)

print(f"\nGennemsnitlig antal tokens: {df['n_tokens'].mean():.1f}")
print(f"Median antal tokens: {df['n_tokens'].median():.1f}")
print(f"Min: {df['n_tokens'].min()}, Max: {df['n_tokens'].max()}") #df = df[df['text'] != ""]

Gennemsnitlig antal tokens: 220.8
Median antal tokens: 121.0
Min: 0, Max: 1552

Teknik-noter

  1. batch_size = 50 betyder, at vi vil sende 50 tekster ad gangen til spaCy-pipelinen.
  2. texts = ... henter kolonnen text_clean fra DataFrame’en og konverterer den til en almindelig Python-liste.
  3. nlp.pipe() er spaCy’s hurtigste måde at køre pipeline’n på mange tekster. I stedet for at kalde nlp(text) én ad gangen, streamer pipe() teksterne igennem modellen i batches.
    • tqdm(... er blot en smart statusbar, der viser hvor lang tid vi skal vente.
    • doc er et spaCy Doc-objekt, som repræsenterer én af teksterne i batchen.
  4. tokens = [token.lemma_ for ... er en list comprehension, som skaber en liste af tokens for hvert dokument.
    • token.lemma_ henter lemmatiseret form af hver token if-betingelserne filtrerer tokens:
      • token.is_stop: udelukker stopord (fx “og”, “men”, “ikke”),
      • token.is_punct: udelukker tegnsætning,
      • token.is_space: udelukker mellemrum.
  5. all_tokens.append(tokens) tilføjer listen af tokens for det aktuelle dokument til all_tokens. Efter loopet vil all_tokens have samme længde som texts, dvs. én liste pr. tekst.
  6. df['tokens'] = all_tokens tilføjer en ny kolonne til df, hvor hver række (dokument) nu indeholder listen af tokens.
  7. df['n_tokens'] = df['tokens'].apply(len) apply(len) beregner længden af hver liste i ‘tokens’ og gemmer resultatet i en ny kolonne ‘n_tokens’.

Vocabular-analyse

from collections import Counter

# Flatten alle tokens til én lang liste
all_words = []
for token_list in df['tokens']:
    all_words.extend(token_list)

print(all_words[:100])
print(f"\nTotal antal ord (med gentagelser): {len(all_words)}")
['årtier', 'fejlslag', 'politik', 'bygge', 'drømmesyner', 'en', 'verden', 'virkelighed', 'vende', 'sin', 'forhammer', 'netop', 'læse', 'to', 'politiker', 'universitetsansats', 'bekymre', 'udsagn', 'årti', 'stå', 'tærskel', 'mogens', 'lykketoft', 'Lars', 'løkke', 'rasmussen', 'professor', 'marl', 'wind', 'behøve', 'stor', 'introduktion', 'socialdemokrat', 'venstremanden', 'politiserende', 'professor', 'fælles', 'bryde', 'uafhængig', 'nationalstat', 'anskue', 'eu', 'afgørende', 'Europa', 'se', 'gerne', 'overstatslighed', 'følgelig', 'afskyre', 'brexit', 'donald', 'trump', 'nationalstat', 'flertal', 'ønske', 'nedlægge', 'søle', 'stor', 'se', 'antidemokratisk', 'katt', 'slæbe', 'mogens', 'lykketoft', 'Europa', 'tydelig', 'genforening', 'udvidelse', 'eu', 'give', 'bølgeskvulp', 'forudse', 'eu', 's', 'mangle', 'evne', 'håndtere', 'beskyttelse', 'ydre', 'grænse', 'samt', 'hel', 'diskussion', 'flygtningestrømmene', 'fylde', 'føre', 'ny', 'modsætninge', 'ny', 'populistisk', 'bevægelse', 'Europa', 'se', 'halvautoritære', 'regime', 'opstå', 'polen', 'ungarn', 'generelt', 'Europa']

Total antal ord (med gentagelser): 145753
# Tæl hvor ofte hvert ord forekommer
word_counts = Counter(all_words)

print(f"Unikke ord (vocabulary size): {len(word_counts)}")
Unikke ord (vocabulary size): 22497
# Top 20 mest anvendte
print("\nTop 20 mest anvendte ord:")
for word, count in word_counts.most_common(20):
    print(f"  {word:20s}: {count:4d}")


# DataFrame med 100 mest almindelige ord til senere visualisering
vocab_df = pd.DataFrame(word_counts.most_common(100), 
                        columns=['word', 'frequency'])

# Se fordelingen
print("\nOrd der forekommer kun én gang (hapax legomena):")
singletons = [word for word, count in word_counts.items() if count == 1]
print(f"  Antal: {len(singletons)}")
print(f"  Eksempler: {singletons[:10]}")

Top 20 mest anvendte ord:
  sin                 :  871
  mod                 :  795
  politisk            :  660
  stor                :  645
  få                  :  623
  ytringsfrihed       :  620
  år                  :  600
  dansk               :  587
  se                  :  546
  medie               :  510
  robinson            :  507
  gå                  :  504
  Danmark             :  481
  når                 :  469
  læse                :  439
  blive               :  393
  land                :  380
  sige                :  360
  rest                :  358
  stå                 :  356

Ord der forekommer kun én gang (hapax legomena):
  Antal: 11815
  Eksempler: ['fejlslag', 'universitetsansats', 'venstremanden', 'overstatslighed', 'afskyre', 'søle', 'bølgeskvulp', 'modsætninge', 'halvautoritære', 'internationalisering']

Komparativt

DKsamling_tokens = []
Trykkefrihed_tokens = []

for idx, row in df.iterrows():
    if row['source'] == 'DKsamling':
        DKsamling_tokens.extend(row['tokens'])
    else:
        Trykkefrihed_tokens.extend(row['tokens'])

DKsamling_vocab = Counter(DKsamling_tokens)
Trykkefrihed_vocab = Counter(Trykkefrihed_tokens)

print(f"DKsamling vocab: {len(DKsamling_vocab)}")
print(f"Trykkefrihed vocab: {len(Trykkefrihed_vocab)}")
DKsamling vocab: 8379
Trykkefrihed vocab: 19176
print("\nTop 10 ord i DKsamling:")
for word, count in DKsamling_vocab.most_common(10):
    print(f"  {word}: {count}")

print("\nTop 10 ord i Trykkefrihed:")
for word, count in Trykkefrihed_vocab.most_common(10):
    print(f"  {word}: {count}")

Top 10 ord i DKsamling:
  læse: 371
  rest: 326
  jyllands-posten: 299
  dansk: 253
  hos: 203
  år: 191
  stor: 188
  få: 182
  se: 176
  Danmark: 169

Top 10 ord i Trykkefrihed:
  sin: 759
  mod: 694
  ytringsfrihed: 605
  politisk: 557
  robinson: 491
  stor: 457
  få: 441
  medie: 441
  år: 409
  se: 370

Teknik-noter

  1. DKsamling_tokens = [], Trykkefrihed_tokens = [] opretter to tomme Python-lister. De skal hver især indeholde alle tokens fra teksterne i den pågældende gruppe.
  2. df.iterrows() går række for række gennem dataframen.
    • idx er rækkens indeks (nummer eller label)
    • row er et pandas Series-objekt, der indeholder alle kolonner for den pågældende række.
    • Hver iteration repræsenterer altså ét dokument (én tekst).
  3. if row['source'] == 'DKsamling': ... tjekker værdien af kolonnen source for hver række. Hvis kilden er “DKsamling”, så tilføjes tokens fra denne tekst til DKsamling_tokens. Ellers (dvs. hvis source ikke er “DKsamling”) tilføjes tokens til Trykkefrihed_tokens.
    • .extend() bruges i stedet for .append() fordi row[‘tokens’] selv er en liste af tokens.
  4. Counter() (fra collections-modulet) laver en ordfrekvens-ordbog. Den tæller hvor mange gange hvert token optræder. Resultatet er et dictionary-lignende objekt.

Præ-processering af data III: feature extraction

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

# Vi skal konvertere tokens tilbage til strings for sklearn kan arbejde med data
df['tokens_string'] = df['tokens'].apply(lambda x: ' '.join(x))

# Lav egen liste
ord_fjernes = ['sige', 'ny', 'se', 'læser'] # Tilføj efter behov

# Kombiner dem
# all_stopwords = list(ENGLISH_STOP_WORDS) + ord_fjernes

# Count Vectorizer (metode 1)
count_vectorizer = CountVectorizer(
    max_features=1000,  # Top 1000 ord
    min_df=2,           # Skal forekomme i mindst 2 dokumenter
    max_df=0.8,         # Max i 80% af dokumenter
    stop_words=ord_fjernes         
)

X_counts = count_vectorizer.fit_transform(df['tokens_string'])

print(f"Matrix shape: {X_counts.shape}")
print(f"  - {X_counts.shape[0]} dokumenter")
print(f"  - {X_counts.shape[1]} features (ord)")

sparsity = 1.0 - (X_counts.nnz / (X_counts.shape[0] * X_counts.shape[1]))
print(f"\nSparsity: {sparsity:.2%}")
print("  (hvor stor andel af matrix er 0)")
Matrix shape: (660, 1000)
  - 660 dokumenter
  - 1000 features (ord)

Sparsity: 91.88%
  (hvor stor andel af matrix er 0)
# TF-IDF Vectorizer (metode 2)

tfidf_vectorizer = TfidfVectorizer(
    max_features=1000,
    min_df=2,
    max_df=0.8
)

X_tfidf = tfidf_vectorizer.fit_transform(df['tokens_string'])

print(f"Matrix shape: {X_tfidf.shape}")
print(f"  - {X_tfidf.shape[0]} dokumenter")
print(f"  - {X_tfidf.shape[1]} features (ord)")

sparsity_tfidf = 1.0 - (X_tfidf.nnz / (X_tfidf.shape[0] * X_tfidf.shape[1]))
print(f"Sparsity: {sparsity_tfidf:.2%}")
Matrix shape: (660, 1000)
  - 660 dokumenter
  - 1000 features (ord)
Sparsity: 91.79%

Sammeligning af feature extraction metode

# Tag et eksempel dokument
doc_idx = 0

# Hent feature names
feature_names = tfidf_vectorizer.get_feature_names_out()

# Count værdier
count_vector = X_counts[doc_idx].toarray().flatten()
top_count_indices = count_vector.argsort()[-10:][::-1]

print("\nTop 10 ord (COUNT):")
for idx in top_count_indices:
    print(f"  {feature_names[idx]:20s}: {count_vector[idx]:.0f}")

Top 10 ord (COUNT):
  europæisk           : 4
  leder               : 3
  europæer            : 3
  magthaver           : 3
  gælde               : 2
  føle                : 2
  løse                : 2
  beslutning          : 2
  straffe             : 2
  profil              : 2

TF-IDF giver lavere vægt til ord der er meget almindelige og højere vægt til ord der er distinkte for dokumentet

# TF-IDF værdier
tfidf_vector = X_tfidf[doc_idx].toarray().flatten()
top_tfidf_indices = tfidf_vector.argsort()[-10:][::-1]

print("\nTop 10 ord (TF-IDF):")
for idx in top_tfidf_indices:
    print(f"  {feature_names[idx]:20s}: {tfidf_vector[idx]:.3f}")

Top 10 ord (TF-IDF):
  europa              : 0.317
  løkke               : 0.306
  lars                : 0.261
  eu                  : 0.232
  fælles              : 0.204
  beskyttelse         : 0.203
  rasmussen           : 0.198
  professor           : 0.186
  ny                  : 0.181
  grænse              : 0.159

\(K\)-means Clustering

from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score

k_values = range(2, 11)
silhouette_scores = []
inertias = []

for k in k_values:
    # Fit K-means
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    cluster_labels = kmeans.fit_predict(X_tfidf)
    
    # Beregn silhouette score (højere er bedre)
    score = silhouette_score(X_tfidf, cluster_labels)
    silhouette_scores.append(score)
    
    # Gem inertia (lavere er bedre)
    inertias.append(kmeans.inertia_)
    
    print(f"K={k}: silhouette={score:.3f}, inertia={kmeans.inertia_:.1f}")

# Lav DataFrame til plotting
elbow_df = pd.DataFrame({
    'k': list(k_values),
    'silhouette': silhouette_scores,
    'inertia': inertias
})
K=2: silhouette=0.015, inertia=597.2
K=3: silhouette=0.018, inertia=586.3
K=4: silhouette=0.023, inertia=578.5
K=5: silhouette=0.027, inertia=571.2
K=6: silhouette=0.026, inertia=567.3
K=7: silhouette=0.028, inertia=562.9
K=8: silhouette=0.031, inertia=558.3
K=9: silhouette=0.032, inertia=552.8
K=10: silhouette=0.033, inertia=547.9
Vis kode
from plotnine import *

# Normaliser begge metrics til 0-1 range for sammenligning
elbow_df['silhouette_norm'] = (elbow_df['silhouette'] - elbow_df['silhouette'].min()) / (elbow_df['silhouette'].max() - elbow_df['silhouette'].min())
elbow_df['inertia_norm'] = (elbow_df['inertia'] - elbow_df['inertia'].min()) / (elbow_df['inertia'].max() - elbow_df['inertia'].min())
elbow_df['inertia_norm_inv'] = 1 - elbow_df['inertia_norm']  # Inverter så højere er bedre

# Reshape til long format
elbow_long = pd.DataFrame({
    'k': list(elbow_df['k']) * 2,
    'metric': ['Silhouette']*len(elbow_df) + ['Inertia (inverteret)']*len(elbow_df),
    'value': list(elbow_df['silhouette_norm']) + list(elbow_df['inertia_norm_inv'])
})

p_combined = (ggplot(elbow_long, aes(x='k', y='value', color='metric'))
 + geom_line(size=1.5)
 + geom_point(size=3)
 + theme_minimal()
 + theme(figure_size=(10, 5))
 + scale_x_continuous(breaks=range(2, 11))
 + labs(title='Sammenligning af Cluster Kvalitet Metrics',
        x='Antal Clusters (K)',
        y='Normaliseret Score (højere = bedre)',
        color='Metric'))

p_combined

# Vælg K
best_k = 5

# Fit med valgt K
kmeans_final = KMeans(n_clusters=best_k, random_state=42, n_init=10)
df['cluster'] = kmeans_final.fit_predict(X_tfidf)

print("\nCluster fordeling:")
print(df['cluster'].value_counts().sort_index())

Cluster fordeling:
cluster
0    277
1     33
2     60
3     41
4    249
Name: count, dtype: int64
# Find karakteristiske ord for hver cluster
for cluster_num in range(best_k):
    # Find alle dokumenter i denne cluster
    cluster_docs = X_tfidf[df['cluster'] == cluster_num]
    
    # Beregn gennemsnitlig TF-IDF værdi per ord
    avg_tfidf = cluster_docs.mean(axis=0).A1
    
    # Find top ord
    top_indices = avg_tfidf.argsort()[-15:][::-1]
    top_words = [feature_names[idx] for idx in top_indices]
    top_scores = [avg_tfidf[idx] for idx in top_indices]
    
    # Print
    n_docs = (df['cluster'] == cluster_num).sum()
    print(f"\n CLUSTER {cluster_num} (n={n_docs} dokumenter)")
    for word, score in zip(top_words, top_scores):
        print(f"   {word:20s}: {score:.3f}")

 CLUSTER 0 (n=277 dokumenter)
   læse                : 0.062
   posten              : 0.057
   jyllands            : 0.057
   rest                : 0.056
   dansk               : 0.045
   danmark             : 0.042
   ikkevestlig         : 0.041
   hos                 : 0.041
   procent             : 0.038
   eu                  : 0.035
   år                  : 0.034
   dansker             : 0.033
   land                : 0.030
   få                  : 0.029
   verden              : 0.029

 CLUSTER 1 (n=33 dokumenter)
   lone                : 0.317
   nørgaard            : 0.296
   video               : 0.116
   eu                  : 0.110
   blad                : 0.091
   ekstra              : 0.085
   dansk               : 0.064
   bog                 : 0.048
   fog                 : 0.047
   2020                : 0.047
   to                  : 0.037
   side                : 0.033
   danmark             : 0.029
   2019                : 0.029
   gerne               : 0.028

 CLUSTER 2 (n=60 dokumenter)
   trump               : 0.192
   hvid                : 0.127
   usa                 : 0.113
   amerikansk          : 0.095
   bid                 : 0.087
   donald              : 0.078
   joe                 : 0.074
   sort                : 0.070
   amerikaner          : 0.065
   præsident           : 0.064
   dr                  : 0.054
   demokrat            : 0.051
   vinde               : 0.049
   læse                : 0.040
   stor                : 0.038

 CLUSTER 3 (n=41 dokumenter)
   robinson            : 0.424
   tommy               : 0.292
   engelsk             : 0.095
   facebook            : 0.085
   trykkefrihedsselskab: 0.061
   britisk             : 0.060
   sin                 : 0.053
   mod                 : 0.051
   england             : 0.047
   ytringsfrihed       : 0.045
   facebooks           : 0.042
   journalist          : 0.042
   censur              : 0.040
   medie               : 0.039
   blokere             : 0.037

 CLUSTER 4 (n=249 dokumenter)
   ytringsfrihed       : 0.068
   sin                 : 0.056
   mod                 : 0.049
   politisk            : 0.044
   medie               : 0.037
   stor                : 0.034
   danmark             : 0.033
   trykkefrihedsselskab: 0.032
   år                  : 0.032
   når                 : 0.032
   dansk               : 0.031
   få                  : 0.031
   sag                 : 0.030
   tage                : 0.029
   debat               : 0.028

Komparativt

# Cross-tab mellem cluster og source

crosstab = pd.crosstab(df['cluster'], df['source'])
print("\nAntal dokumenter:")
print(crosstab)

# Normaliser til procent
crosstab_pct = pd.crosstab(df['cluster'], df['source'], normalize='columns') * 100
print("\nProcent af hver kilde:")
print(crosstab_pct.round(1))

Antal dokumenter:
source   DKsamling  Trykkefrihed
cluster                         
0              272             5
1               33             0
2               37            23
3                4            37
4               20           229

Procent af hver kilde:
source   DKsamling  Trykkefrihed
cluster                         
0             74.3           1.7
1              9.0           0.0
2             10.1           7.8
3              1.1          12.6
4              5.5          77.9

Visualisering

from sklearn.decomposition import PCA

# Reducer dimensioner for at kunne visualisere
pca = PCA(n_components=2, random_state=42)
coords_2d = pca.fit_transform(X_tfidf.toarray())

explained_var = pca.explained_variance_ratio_.sum()
print(f"Forklaret varians med 2 komponenter: {explained_var:.1%}")

# Lav DataFrame til plotting
plot_df = pd.DataFrame({
    'PC1': coords_2d[:, 0],
    'PC2': coords_2d[:, 1],
    'cluster': df['cluster'].astype(str),
    'source': df['source'],
    'n_tokens': df['n_tokens']
})
Forklaret varians med 2 komponenter: 4.5%
  • Vi har 1000 dimensioner (features) i vores data
  • Vi reducerer til kun 2 dimensioner for at kunne plotte
  • Men clustering arbejder i det fulde højdimensionelle rum. Så vores clusters er stadig valide, selvom plottet er simplificeret
pca_full = PCA(n_components=50, random_state=42)  # Test med 50
pca_full.fit(X_tfidf.toarray())

cumsum_var = np.cumsum(pca_full.explained_variance_ratio_)

print("Forklaret varians ved forskellige antal komponenter:")
for n in [2, 5, 10, 20, 30, 50]:
    if n <= len(cumsum_var):
        print(f"  {n:2d} komponenter: {cumsum_var[n-1]:.1%}")
Forklaret varians ved forskellige antal komponenter:
   2 komponenter: 4.5%
   5 komponenter: 8.8%
  10 komponenter: 13.9%
  20 komponenter: 21.1%
  30 komponenter: 26.8%
  50 komponenter: 35.7%
Vis kode
var_df = pd.DataFrame({
    'n_components': range(1, min(51, len(cumsum_var)+1)),
    'cum_variance': cumsum_var[:50]
})

p_var = (ggplot(var_df, aes(x='n_components', y='cum_variance'))
 + geom_line(color='steelblue', size=1.5)
 + geom_point(color='steelblue', size=2)
 + theme_minimal()
 + theme(figure_size=(6, 4))
 + scale_y_continuous(labels=lambda l: [f'{x:.0%}' for x in l])
 + labs(title='Kumulativ Forklaret Varians (PCA)',
        x='Antal PCA Komponenter',
        y='Forklaret Varians',
        caption='For tekstdata er det normalt at have spredt information over mange dimensioner'))

p_var

Vis kode
# Scatter plot af clusters
p1 = (ggplot(plot_df, aes(x='PC1', y='PC2', color='cluster', shape='source'))
 + geom_point(size=3, alpha=0.7)
 + theme_minimal()
 + theme(figure_size=(10, 6))
 + labs(title='Dokument Clusters (K-means på TF-IDF)',
        x='Principal Component 1',
        y='Principal Component 2',
        color='Cluster',
        shape='Kilde'))

p1

Vis kode
# Bar chart af cluster størrelser
cluster_counts = df.groupby(['cluster', 'source']).size().reset_index(name='count')

p2 = (ggplot(cluster_counts, aes(x='factor(cluster)', y='count', fill='source'))
 + geom_col(position='dodge')
 + theme_minimal()
 + theme(figure_size=(10, 6))
 + labs(title='Dokumentfordeling per Cluster og Kilde',
        x='Cluster',
        y='Antal dokumenter',
        fill='Kilde'))

p2

Vis kode
# Boxplot af tekstlængde per cluster
p3 = (ggplot(df, aes(x='factor(cluster)', y='n_tokens', fill='factor(cluster)'))
 + geom_boxplot()
 + theme_minimal()
 + theme(figure_size=(10, 6))
 + labs(title='Tekstlængde per Cluster',
        x='Cluster',
        y='Antal tokens')
 + guides(fill=False))

p3

Vis kode
# Bar chart af top ord
p4 = (ggplot(vocab_df.head(30), 
             aes(x='reorder(word, frequency)', y='frequency'))
 + geom_col(fill='steelblue')
 + coord_flip()
 + theme_minimal()
 + theme(figure_size=(6, 6))
 + labs(title='Top 30 Mest Frekvente Ord (efter pre-processing)',
        x='',
        y='Frekvens'))

p4

Vis kode
# Sammenlign gennemsnitlig tekstlængde
avg_length = df.groupby('source')['n_tokens'].mean().reset_index()

p5 = (ggplot(avg_length, aes(x='source', y='n_tokens', fill='source'))
 + geom_col()
 + theme_minimal()
 + theme(figure_size=(8, 6))
 + labs(title='Gennemsnitlig Tekstlængde per Kilde',
        x='',
        y='Antal tokens')
 + guides(fill=False))

p5

Øvelse

Se hvordan analysen er påvirket af:

  • Vores pre-processing beslutninger:
    • Hvad hvis vi havde beholdt stopord?
    • Hvad hvis vi ikke havde lemmatiseret?
    • Osv … prøv ting ad og se om resultaterne ændrer sig.

Ændrer ovenstående på fortolkningerne:

  • Giver vores clusters mere/mindre mening?
    • Kan vi overhovedet fortolke dem substantielt hvis der er flere/færre k?

Diskuter hvad kan forbedre analysen og hvilken viden analyse har bidraget med om de to tekstkilder?

Analyse-eksempel 2: Dekonstruktion af (politiske) narrativer

Hvilke narrativer (og konspirationsteorier) dominerer på […]?

  • Narrative structure: Hvem er “fjenden”? Hvem er “helten”? Hvad er “løsningen”?

  • (Cross-reference med kendte ekstremist-manifesters)

Named Entity Recognition - Hvem tales der om?

import pandas as pd
import spacy
from collections import Counter, defaultdict
from tqdm import tqdm

# Load spaCy model
nlp = spacy.load("da_core_news_lg")

# Extract entities fra alle dokumenter
all_entities = []

for idx, row in tqdm(df.iterrows(), total=len(df)):
    doc = nlp(row['text_clean'])
    
    for ent in doc.ents:
        all_entities.append({
            'text': ent.text,
            'label': ent.label_,
            'source': row['source'],
            'doc_id': idx
        })

entities_df = pd.DataFrame(all_entities)

print(f"\nTotal entities fundet: {len(entities_df)}")
print(f"\nEntity typer:")
print(entities_df['label'].value_counts())

Total entities fundet: 12062

Entity typer:
label
MISC    4862
PER     3377
LOC     2321
ORG     1502
Name: count, dtype: int64
# Filter til relevante entity typer
relevant_entities = entities_df[entities_df['label'].isin(['PER', 'ORG', 'LOC'])]

# Count per entity type og source
for label in ['PER', 'ORG']:
    print(f"\n{label} (Personer/Organisationer):")
    
    for source in ['DKsamling', 'Trykkefrihed']:
        entities_source = relevant_entities[
            (relevant_entities['label'] == label) & 
            (relevant_entities['source'] == source)
        ]
        
        top_entities = entities_source['text'].value_counts().head(10)
        
        print(f"\n  {source}:")
        for entity, count in top_entities.items():
            print(f"    {entity:30s}: {count:3d}")

PER (Personer/Organisationer):

  DKsamling:
    lone nørgaard                 :  41
    mette frederiksen             :  35
    donald trump                  :  28
    trump                         :  21
    usa                           :  16
    inger støjberg                :  16
    ole hasselbalch               :  15
    johnson                       :  15
    tommy robinson                :  14
    eu                            :  13

  Trykkefrihed:
    tommy robinson                : 239
    robinson                      : 115
    trump                         :  75
    charlie hebdo                 :  63
    tommy robinsons               :  51
    samuel paty                   :  44
    donald trump                  :  33
    rasmus paludan                :  32
    mette frederiksen             :  31
    usa                           :  30

ORG (Personer/Organisationer):

  DKsamling:
    jyllands-posten               : 301
    eu                            :  42
    df                            :  41
    dr                            :  40
    berlingske                    :  34
    folketinget                   :  20
    jp                            :  18
    dr’s                          :  14
    politiken                     :  13
    socialdemokratiet             :  13

  Trykkefrihed:
    folketinget                   :  40
    ´s                            :  24
    jyllands-posten               :  22
    eu                            :  21
    folketingets                  :  20
    berlingske                    :  20
    dr                            :  19
    df                            :  16
    socialdemokratiet             :  15
    bbc                           :  14
print("\nTop personer:")
person_entities = entities_df[entities_df['label'] == 'PER']
person_counts = person_entities['text'].value_counts().head(20)
print(person_counts)

print("\nTop organisationer:")
org_entities = entities_df[entities_df['label'] == 'ORG']
org_counts = org_entities['text'].value_counts().head(20)
print(org_counts)

Top personer:
text
tommy robinson       253
robinson             115
trump                 96
mette frederiksen     66
charlie hebdo         65
donald trump          61
tommy robinsons       56
usa                   46
samuel paty           44
lone nørgaard         43
rasmus paludan        37
joe                   35
ole hasselbalch       27
zuckerberg            25
kim                   25
eric zemmour          25
pia kjærsgaard        22
kurt westergaard      21
steen raaschou        21
hong kong             21
Name: count, dtype: int64

Top organisationer:
text
jyllands-posten           323
eu                         63
folketinget                60
dr                         59
df                         57
berlingske                 54
socialdemokratiet          28
jp                         28
folketingets               24
politiken                  24
´s                         24
dr’s                       20
sf                         18
s                          16
nato                       16
tv2                        15
bbc                        14
københavns universitet     13
antifas                    13
p1                         13
Name: count, dtype: int64
# Definer mapping af variationer til canonical form
entity_mapping = {
    # Tommy Robinson variationer
    'robinson': 'tommy robinson',
    'tommy robinsons': 'tommy robinson',
    
    # Trump variationer
    'trump': 'donald trump',
    'donald j. trump': 'donald trump',
    'donald j trump': 'donald trump',
    
    # Andre tænkte eksempler eksempler
    'merkel': 'angela merkel',
    'frederiksen': 'mette frederiksen',
    'løkke': 'lars løkke rasmussen',
    
    # Tilføj hvor nødvendigt ... 
}

entities_df['text'] = entities_df['text'].replace(entity_mapping)
# Definer entities vi vil fjerne
entities_to_remove = [
    'charlie hebdo',  # ikke en person
    'eu',  # For generisk
    'dk',  # Forkortelse
    'usa'  # Land, ikke person
    # osv
]

# Filtrer dem ud
entities_df = entities_df[
    ~entities_df['text'].isin(entities_to_remove)
]

# Se resultat
print("\n\nNye top entities:")
top_persons = entities_df[entities_df['label'] == 'PER']['text'].value_counts().head(10)
print(top_persons)


Nye top entities:
text
tommy robinson       424
donald trump         157
mette frederiksen     67
samuel paty           44
lone nørgaard         43
rasmus paludan        37
joe                   35
ole hasselbalch       27
kim                   25
zuckerberg            25
Name: count, dtype: int64
# Filter til relevante entity typer (SAMME KODE IGEN!)
relevant_entities = entities_df[entities_df['label'].isin(['PER', 'ORG', 'LOC'])]

# Count per entity type og source
for label in ['PER', 'ORG']:
    print(f"\n{label} (Personer/Organisationer):")
    
    for source in ['DKsamling', 'Trykkefrihed']:
        entities_source = relevant_entities[
            (relevant_entities['label'] == label) & 
            (relevant_entities['source'] == source)
        ]
        
        top_entities = entities_source['text'].value_counts().head(10)
        
        print(f"\n  {source}:")
        for entity, count in top_entities.items():
            print(f"    {entity:30s}: {count:3d}")

PER (Personer/Organisationer):

  DKsamling:
    donald trump                  :  49
    lone nørgaard                 :  41
    mette frederiksen             :  35
    tommy robinson                :  19
    inger støjberg                :  16
    ole hasselbalch               :  15
    johnson                       :  15
    joe                           :  12
    lars løkke                    :  12
    lund                          :  11

  Trykkefrihed:
    tommy robinson                : 405
    donald trump                  : 108
    samuel paty                   :  44
    rasmus paludan                :  32
    mette frederiksen             :  32
    eric zemmour                  :  25
    zuckerberg                    :  25
    kim                           :  24
    joe                           :  23
    hong kong                     :  21

ORG (Personer/Organisationer):

  DKsamling:
    jyllands-posten               : 301
    df                            :  41
    dr                            :  40
    berlingske                    :  34
    folketinget                   :  20
    jp                            :  18
    dr’s                          :  14
    politiken                     :  13
    socialdemokratiet             :  13
    københavns universitet        :   8

  Trykkefrihed:
    folketinget                   :  40
    ´s                            :  24
    jyllands-posten               :  22
    folketingets                  :  20
    berlingske                    :  20
    dr                            :  19
    df                            :  16
    socialdemokratiet             :  15
    nato                          :  14
    bbc                           :  14
print("\nTop personer:")
person_entities = entities_df[entities_df['label'] == 'PER']
person_counts = person_entities['text'].value_counts().head(20)
print(person_counts)

print("\nTop organisationer:")
org_entities = entities_df[entities_df['label'] == 'ORG']
org_counts = org_entities['text'].value_counts().head(20)
print(org_counts)

Top personer:
text
tommy robinson       424
donald trump         157
mette frederiksen     67
samuel paty           44
lone nørgaard         43
rasmus paludan        37
joe                   35
ole hasselbalch       27
kim                   25
zuckerberg            25
eric zemmour          25
pia kjærsgaard        22
steen raaschou        21
kurt westergaard      21
hong kong             21
lars vilks            20
youtube               20
antifa                19
inger støjberg        19
aia fog               19
Name: count, dtype: int64

Top organisationer:
text
jyllands-posten                   323
folketinget                        60
dr                                 59
df                                 57
berlingske                         54
jp                                 28
socialdemokratiet                  28
folketingets                       24
´s                                 24
politiken                          24
dr’s                               20
sf                                 18
s                                  16
nato                               16
tv2                                15
bbc                                14
københavns universitet             13
p1                                 13
antifas                            13
københavns professionshøjskole     12
Name: count, dtype: int64

Hvordan tales der om aktørerne?

# Vælg top aktører at analysere
top_persons = person_counts.head(5).index.tolist()
top_orgs = org_counts.head(5).index.tolist()

# Extract kontekst (ord før og efter entity)
context_words = defaultdict(list)

for idx, row in tqdm(df.iterrows(), total=len(df), desc="Extracting context"):
    doc = nlp(row['text_clean'])
    
    for ent in doc.ents:
        if ent.text.lower() in top_persons + top_orgs:
            # Hent 5 ord før og efter
            start_idx = max(0, ent.start - 5)
            end_idx = min(len(doc), ent.end + 5)
            
            context_tokens = doc[start_idx:end_idx]
            
            # Gem ord (undtagen entity selv)
            for token in context_tokens:
                if (not token.is_stop and 
                    not token.is_punct and 
                    token.i not in range(ent.start, ent.end)):
                    context_words[ent.text.lower()].append(token.lemma_.lower())

# Vis hyppigste kontekst-ord
for entity in top_persons[:3]:  # Vis top 3 personer
    if entity in context_words:
        words = Counter(context_words[entity])
        print(f"\n{entity.upper()}:")
        for word, count in words.most_common(15):
            print(f"   {word:20s}: {count}")

TOMMY ROBINSON:
   engelsk             : 29
   ytringsfrihedsaktivist: 18
   mod                 : 12
   trykkefrihedsselskab: 10
   sin                 : 8
   navn                : 7
   sag                 : 7
   blive               : 6
   censur              : 6
   år                  : 6
   artikel             : 6
   sige                : 6
   england             : 6
   vold                : 6
   få                  : 6

DONALD TRUMP:
   præsident           : 9
   vinde               : 5
   sin                 : 4
   håbe                : 4
   mod                 : 4
   valg                : 3
   støtte              : 3
   censur              : 3
   tale                : 3
   republikaner        : 2
   stor                : 2
   holde               : 2
   januar              : 2
   vise                : 2
   debat               : 2

METTE FREDERIKSEN:
   statsminister       : 16
   regering            : 6
   sin                 : 4
   når                 : 4
   leder               : 4
   sætte               : 3
   læse                : 3
   rest                : 3
   jyllands-posten     : 3
   socialdemokrati     : 3
   politisk            : 3
   danmark             : 3
   lex                 : 3
   imidlertid          : 2
   godt                : 2
# Manuel liste af positive/negative ord (kan udvides!)
positive_words = [
    'god', 'godt', 'bedre', 'støtte', 'hjælp', 'hjælpe', 
    'succes', 'vinder', 'vigtig', 'stærk', 'effektiv',
    'demokrati', 'frihed', 'værdig', 'ansvarlig'
]

negative_words = [
    'dårlig', 'dårligt', 'værre', 'problem', 'krise', 'fejl',
    'hadsk', 'farlig', 'trussel', 'angreb', 'katastrofe',
    'populist', 'ekstrem', 'radikal', 'fascisme', 'autoritær'
]

for entity in top_persons[:3]:
    if entity in context_words:
        words = context_words[entity]
        
        pos_count = sum(1 for w in words if w in positive_words)
        neg_count = sum(1 for w in words if w in negative_words)
        
        print(f"\n{entity.upper()}:")
        print(f"  Positive ord: {pos_count}")
        print(f"  Negative ord: {neg_count}")
        
        # Vis eksempler
        pos_examples = [w for w in words if w in positive_words]
        neg_examples = [w for w in words if w in negative_words]
        
        if pos_examples:
            print(f"  Positive: {Counter(pos_examples).most_common(5)}")
        if neg_examples:
            print(f"  Negative: {Counter(neg_examples).most_common(5)}")

TOMMY ROBINSON:
  Positive ord: 5
  Negative ord: 3
  Positive: [('vigtig', 2), ('støtte', 2), ('demokrati', 1)]
  Negative: [('angreb', 2), ('fejl', 1)]

DONALD TRUMP:
  Positive ord: 3
  Negative ord: 0
  Positive: [('støtte', 3)]

METTE FREDERIKSEN:
  Positive ord: 3
  Negative ord: 1
  Positive: [('godt', 2), ('stærk', 1)]
  Negative: [('problem', 1)]

Narrativer

# Problem/trussel ord (indikerer "fjende")
problem_words = [
    'problem', 'trussel', 'krise', 'fare', 'farlig', 'katastrofe',
    'angreb', 'ødelægge', 'underminere', 'svigter', 'fejler',
    'populist', 'ekstremist', 'radikal', 'hadsk'
]

# Løsning/helt ord (indikerer "helt")
solution_words = [
    'løsning', 'løse', 'hjælp', 'hjælpe', 'redde', 'beskytte',
    'forsvare', 'støtte', 'sikre', 'forbedre', 'styrke',
    'demokrati', 'frihed', 'ansvarlig', 'leder', 'vision'
]

# Count co-occurrences med entities
entity_problem_score = defaultdict(int)
entity_solution_score = defaultdict(int)

for entity in top_persons + top_orgs:
    if entity in context_words:
        words = context_words[entity]
        
        entity_problem_score[entity] = sum(1 for w in words if w in problem_words)
        entity_solution_score[entity] = sum(1 for w in words if w in solution_words)

# Visualiser som dataframe
narrative_df = pd.DataFrame({
    'entity': list(entity_problem_score.keys()),
    'problem_words': list(entity_problem_score.values()),
    'solution_words': list(entity_solution_score.values())
})

narrative_df['net_framing'] = narrative_df['solution_words'] - narrative_df['problem_words']
narrative_df = narrative_df.sort_values('net_framing')

print("\nNARRATIV FRAMING (negativ = 'fjende', positiv = 'helt'):")
print(narrative_df)

NARRATIV FRAMING (negativ = 'fjende', positiv = 'helt'):
              entity  problem_words  solution_words  net_framing
7                 dr              2               1           -1
4      lone nørgaard              1               1            0
8                 df              1               1            0
9         berlingske              0               1            1
3        samuel paty              0               2            2
5    jyllands-posten              1               3            2
6        folketinget              1               3            2
2  mette frederiksen              1               4            3
1       donald trump              0               4            4
0     tommy robinson              2               8            6
# Gentag men split efter source

for source in ['DKsamling', 'Trykkefrihed']:
    print(f"\n{source.upper()}:")
    
    # Filter entities fra denne kilde
    source_entities = entities_df[entities_df['source'] == source]
    source_top_persons = source_entities[
        source_entities['label'] == 'PER'
    ]['text'].value_counts().head(5).index.tolist()
    
    print(f"\nTop personer: {source_top_persons}")
    
    # Extract context for denne kilde
    source_context = defaultdict(list)
    
    for idx, row in df[df['source'] == source].iterrows():
        doc = nlp(row['text_clean'])
        
        for ent in doc.ents:
            if ent.text.lower() in source_top_persons:
                start_idx = max(0, ent.start - 5)
                end_idx = min(len(doc), ent.end + 5)
                
                context_tokens = doc[start_idx:end_idx]
                
                for token in context_tokens:
                    if (not token.is_stop and 
                        not token.is_punct and 
                        token.i not in range(ent.start, ent.end)):
                        source_context[ent.text.lower()].append(token.lemma_.lower())
    
    # Score
    print("\nFraming scores:")
    for entity in source_top_persons[:3]:
        if entity in source_context:
            words = source_context[entity]
            prob_score = sum(1 for w in words if w in problem_words)
            sol_score = sum(1 for w in words if w in solution_words)
            
            print(f"  {entity:20s}: problem={prob_score:2d}, solution={sol_score:2d}, net={sol_score-prob_score:+3d}")

DKSAMLING:

Top personer: ['donald trump', 'lone nørgaard', 'mette frederiksen', 'tommy robinson', 'inger støjberg']

Framing scores:
  donald trump        : problem= 0, solution= 2, net= +2
  lone nørgaard       : problem= 1, solution= 1, net= +0
  mette frederiksen   : problem= 0, solution= 1, net= +1

TRYKKEFRIHED:

Top personer: ['tommy robinson', 'donald trump', 'samuel paty', 'rasmus paludan', 'mette frederiksen']

Framing scores:
  tommy robinson      : problem= 2, solution= 7, net= +5
  donald trump        : problem= 0, solution= 2, net= +2
  samuel paty         : problem= 0, solution= 2, net= +2

Identificer diskuser/temaer

# Udvid keyword lister med relaterede termer
threat_keywords = [
    'trussel', 'fare', 'farlig', 'krise', 'problem', 'katastrofe',
    'invasion', 'immigration', 'immigrant', 'flygtning', 'migrant',
    'islam', 'terror', 'kriminalitet', 'vold', 'angreb',
    'populisme', 'populist', 'ekstremisme', 'fascisme', 'had'
]

solution_keywords = [
    'løsning', 'demokrati', 'frihed', 'sikkerhed', 'beskyttelse',
    'grænse', 'kontrol', 'lov', 'orden', 'ansvar',
    'national', 'nationalstat', 'suverænitet', 'identitet',
    'integration', 'assimilering', 'værdier', 'kultur'
]

eu_keywords = [
    'eu', 'europa', 'bruxelles', 'unionen', 'europæisk',
    'fælles', 'overstatlig', 'samarbejde', 'integration'
]
for source in ['DKsamling', 'Trykkefrihed']:
    print(f"\n{source.upper()}:")
    
    # Get all tokens from this source
    source_tokens = []
    for tokens in df[df['source'] == source]['tokens']:
        source_tokens.extend(tokens)
    
    total_tokens = len(source_tokens)
    
    # Count keywords
    threat_count = sum(1 for t in source_tokens if t in threat_keywords)
    solution_count = sum(1 for t in source_tokens if t in solution_keywords)
    eu_count = sum(1 for t in source_tokens if t in eu_keywords)
    
    print(f"  Total tokens: {total_tokens}")
    print(f"  Trussel-ord: {threat_count} ({threat_count/total_tokens*100:.2f}%)")
    print(f"  Løsnings-ord: {solution_count} ({solution_count/total_tokens*100:.2f}%)")
    print(f"  EU-ord: {eu_count} ({eu_count/total_tokens*100:.2f}%)")
    
    # Hvilke specifikke ord?
    threat_counter = Counter([t for t in source_tokens if t in threat_keywords])
    solution_counter = Counter([t for t in source_tokens if t in solution_keywords])
    
    print(f"\n  Top trussel-ord: {threat_counter.most_common(5)}")
    print(f"  Top løsnings-ord: {solution_counter.most_common(5)}")

DKSAMLING:
  Total tokens: 34615
  Trussel-ord: 191 (0.55%)
  Løsnings-ord: 220 (0.64%)
  EU-ord: 149 (0.43%)

  Top trussel-ord: [('problem', 31), ('vold', 31), ('kriminalitet', 28), ('flygtning', 12), ('had', 12)]
  Top løsnings-ord: [('lov', 47), ('grænse', 36), ('demokrati', 22), ('integration', 16), ('national', 15)]

TRYKKEFRIHED:
  Total tokens: 111138
  Trussel-ord: 917 (0.83%)
  Løsnings-ord: 884 (0.80%)
  EU-ord: 192 (0.17%)

  Top trussel-ord: [('problem', 156), ('angreb', 140), ('islam', 137), ('vold', 134), ('had', 85)]
  Top løsnings-ord: [('demokrati', 185), ('lov', 156), ('frihed', 109), ('grænse', 73), ('ansvar', 60)]

Hvem nævnes sammen med hvem?

# Build co-occurrence matrix
from itertools import combinations

# Get entities per document
doc_entities = defaultdict(list)

for idx, row in entities_df.iterrows():
    if row['label'] in ['PER', 'ORG']:
        doc_entities[row['doc_id']].append(row['text'])

# Count co-occurrences
cooccurrence_counts = Counter()

for doc_id, entities in doc_entities.items():
    # Get unique entities in doc
    unique_entities = list(set(entities))
    
    # Count all pairs
    for ent1, ent2 in combinations(sorted(unique_entities), 2):
        cooccurrence_counts[(ent1, ent2)] += 1

# Vis top co-occurrences
print("Top 20 entity par der nævnes sammen:")
for (ent1, ent2), count in cooccurrence_counts.most_common(20):
    print(f"  {ent1:25s} <-> {ent2:25s}: {count}")
Top 20 entity par der nævnes sammen:
  berlingske                <-> jyllands-posten          : 28
  donald trump              <-> jyllands-posten          : 24
  dr                        <-> jyllands-posten          : 21
  jyllands-posten           <-> mette frederiksen        : 21
  folketinget               <-> jyllands-posten          : 14
  jp                        <-> jyllands-posten          : 14
  donald trump              <-> joe                      : 13
  dr                        <-> dr’s                     : 12
  dr’s                      <-> jyllands-posten          : 12
  jyllands-posten           <-> politiken                : 12
  jyllands-posten           <-> socialdemokratiet        : 11
  jyllands-posten           <-> lars løkke               : 9
  donald trump              <-> dr                       : 9
  inger støjberg            <-> jyllands-posten          : 9
  df                        <-> jyllands-posten          : 9
  donald trump              <-> usa’s                    : 9
  joe                       <-> jyllands-posten          : 9
  jyllands-posten           <-> usa’s                    : 9
  mette frederiksen         <-> mette frederiksens       : 8
  jyllands-posten           <-> lars løkke rasmussen     : 7
for source in ['DKsamling', 'Trykkefrihed']:
    print(f"\n{source.upper()}:")
    
    # Filter til denne kilde
    source_doc_entities = defaultdict(list)
    
    source_entity_rows = entities_df[entities_df['source'] == source]
    for idx, row in source_entity_rows.iterrows():
        if row['label'] in ['PER', 'ORG']:
            source_doc_entities[row['doc_id']].append(row['text'])
    
    # Count co-occurrences
    source_cooccurrence = Counter()
    
    for doc_id, entities in source_doc_entities.items():
        unique_entities = list(set(entities))
        
        for ent1, ent2 in combinations(sorted(unique_entities), 2):
            source_cooccurrence[(ent1, ent2)] += 1
    
    print(f"\nTop 10 par:")
    for (ent1, ent2), count in source_cooccurrence.most_common(10):
        print(f"  {ent1:25s} <-> {ent2:25s}: {count}")

DKSAMLING:

Top 10 par:
  berlingske                <-> jyllands-posten          : 27
  donald trump              <-> jyllands-posten          : 22
  dr                        <-> jyllands-posten          : 21
  jyllands-posten           <-> mette frederiksen        : 21
  folketinget               <-> jyllands-posten          : 13
  dr’s                      <-> jyllands-posten          : 12
  jp                        <-> jyllands-posten          : 12
  jyllands-posten           <-> politiken                : 11
  jyllands-posten           <-> socialdemokratiet        : 10
  jyllands-posten           <-> lars løkke               : 9

TRYKKEFRIHED:

Top 10 par:
  folketinget               <-> tommy robinson           : 7
  peter münster             <-> tommy robinson           : 7
  mette frederiksen         <-> mette frederiksens       : 7
  donald trump              <-> joe                      : 7
  donald trump              <-> zuckerberg               : 7
  folketinget               <-> nato                     : 6
  donald trump              <-> dr                       : 5
  donald trump              <-> steffen kretz            : 5
  folketinget               <-> folketingets             : 5
  folketingets              <-> mette frederiksen        : 5

Visualisering

# Top entities per source
top_n = 10

DKsamling_entities = entities_df[
    (entities_df['source'] == 'DKsamling') & 
    (entities_df['label'] == 'PER')
]['text'].value_counts().head(top_n)

Trykkefrihed_entities = entities_df[
    (entities_df['source'] == 'Trykkefrihed') & 
    (entities_df['label'] == 'PER')
]['text'].value_counts().head(top_n)

# Kombiner til plot
entity_comparison = pd.DataFrame({
    'entity': list(DKsamling_entities.index) + list(Trykkefrihed_entities.index),
    'count': list(DKsamling_entities.values) + list(Trykkefrihed_entities.values),
    'source': ['DKsamling Media']*top_n + ['Alternative Blogs']*top_n
})
Vis kode
p1 = (ggplot(entity_comparison, aes(x='reorder(entity, count)', y='count', fill='source'))
 + geom_col()
 + coord_flip()
 + facet_wrap('~source', scales='free_y')
 + theme_minimal()
 + theme(figure_size=(6, 6))
 + labs(title='Top Personer Nævnt per Kilde',
        x='',
        y='Antal nævnelser')
 + guides(fill=False))

p1

Vis kode
# Brug narrative_df fra tidligere
# Filter til dem med nok nævnelser
narrative_plot_df = narrative_df[
    (narrative_df['problem_words'] + narrative_df['solution_words']) >= 5
].copy()

narrative_plot_df['entity_short'] = narrative_plot_df['entity'].str[:30]

p2 = (ggplot(narrative_plot_df, aes(x='reorder(entity_short, net_framing)', y='net_framing'))
 + geom_col(aes(fill='net_framing'))
 + coord_flip()
 + scale_fill_gradient2(low='red', mid='gray', high='green', midpoint=0)
 + theme_minimal()
 + theme(figure_size=(6, 6))
 + labs(title='Narrativ Framing af Aktører',
        subtitle='Negativ = "fjende" framing, Positiv = "helt" framing',
        x='',
        y='Net Framing Score')
 + guides(fill=False))

p2

Vis kode
# Beregn keyword densitet per kilde
discourse_data = []

for source in ['DKsamling', 'Trykkefrihed']:
    source_tokens = []
    for tokens in df[df['source'] == source]['tokens']:
        source_tokens.extend(tokens)
    
    total = len(source_tokens)
    
    discourse_data.append({
        'source': source,
        'category': 'Trussel/Problem',
        'percentage': sum(1 for t in source_tokens if t in threat_keywords) / total * 100
    })
    
    discourse_data.append({
        'source': source,
        'category': 'Løsning/Handling',
        'percentage': sum(1 for t in source_tokens if t in solution_keywords) / total * 100
    })
    
    discourse_data.append({
        'source': source,
        'category': 'EU/Europa',
        'percentage': sum(1 for t in source_tokens if t in eu_keywords) / total * 100
    })

discourse_df = pd.DataFrame(discourse_data)

p3 = (ggplot(discourse_df, aes(x='category', y='percentage', fill='source'))
 + geom_col(position='dodge')
 + theme_minimal()
 + theme(figure_size=(10, 6), axis_text_x=element_text(angle=0))
 + labs(title='Tematisk Diskurs Sammenligning',
        x='',
        y='Procent af total tokens',
        fill='Kilde'))

p3

Vis kode
# Lav en simpel heatmap af top co-occurrences
# Tag top 10 entities
top_entities_for_network = person_counts.head(10).index.tolist()

# Build matrix
cooc_matrix = pd.DataFrame(0, 
                           index=top_entities_for_network,
                           columns=top_entities_for_network)

for (ent1, ent2), count in cooccurrence_counts.items():
    if ent1 in top_entities_for_network and ent2 in top_entities_for_network:
        cooc_matrix.loc[ent1, ent2] = count
        cooc_matrix.loc[ent2, ent1] = count

# Reshape til long format for plotnine
cooc_long = []
for i in cooc_matrix.index:
    for j in cooc_matrix.columns:
        if i < j:  # Kun upper triangle for at undgå duplicates
            cooc_long.append({
                'entity1': i,
                'entity2': j,
                'count': cooc_matrix.loc[i, j]
            })

cooc_plot_df = pd.DataFrame(cooc_long)
cooc_plot_df = cooc_plot_df[cooc_plot_df['count'] > 0]  # Kun eksisterende forbindelser

p4 = (ggplot(cooc_plot_df, aes(x='entity1', y='entity2', fill='count'))
 + geom_tile()
 + scale_fill_gradient(low='white', high='darkblue')
 + theme_minimal()
 + theme(figure_size=(6, 6),
         axis_text_x=element_text(angle=45, hjust=1))
 + labs(title='Co-occurrence Network: Hvem nævnes sammen?',
        x='', y='',
        fill='Co-occurrences'))

p4

Øvelse

Se hvordan analysen er påvirket af:

  • Pre-processing (som første øvelse)
  • “Rengøring” af vores data (hvilke entities beholder vi)

Diskuter hvad kan forbedre analysen og hvilken viden analysen har bidraget med om de to tekstkilder?

Diskuter hvad denne metode kan som den anden ikke kan. Skal man gå til tekstdataen på en anden måde?